Skip to content

Opportunisticly emit compact imports#8926

Merged
tlively merged 20 commits into
mainfrom
opportunistic-compact-imports
Jul 24, 2026
Merged

Opportunisticly emit compact imports#8926
tlively merged 20 commits into
mainfrom
opportunistic-compact-imports

Conversation

@tlively

@tlively tlively commented Jul 22, 2026

Copy link
Copy Markdown
Member

In the binary writer when compact imports are enabled, opportunistically look for pairs of adjacent imports that share both their module names and import types, or alternatively just their module names. When a pair of matching imports is found, create a run of compact imports that includes as many subsequent imports as possible. Adding a pass that will reorder imports to purposefully put similar imports together is left as future work.

tlively added 7 commits July 21, 2026 18:34
Update the parser for imports to support both forms of compact imports. Because imports are no longer 1:1 with import statements, the locations of imported items the parser collects and re-parses in various phases must now be the locations of importdesc productions rather than import productions. Since e.g. a function importdesc cannot be differentiated from a function definition with an empty body without context, we must add a mechanism for the parser context to tell the parser whether or not it is parsing an import.
When e.g. a function is declared in the text format, it can either be with a normal `func` production (which may or may not have an inline import declaration) or it can be an `import` production. Previously the parser would just store the location of the declaration but not the kind of production it used. In later parser phases where the declaration has to be parsed again, we would just try both productions to see which one worked. This is rather hacky and brittle and will not play nicely with the text format for compact imports, so refactor to explicitly track how the declaration should be parsed.
@tlively tlively changed the title [WIP SLOP] Opportunisticly emit compact imports Opportunisticly emit compact imports Jul 23, 2026
@tlively
tlively marked this pull request as ready for review July 23, 2026 01:56
@tlively
tlively requested a review from a team as a code owner July 23, 2026 01:56
@tlively
tlively requested review from kripken and removed request for a team July 23, 2026 01:56
Comment thread src/wasm/wasm-binary.cpp Outdated
if (const auto* fa = std::get_if<Function*>(&a)) {
auto* fb = std::get<Function*>(b);
return (*fa)->type.isExact() == fb->type.isExact() &&
(*fa)->type.getHeapType() == fb->type.getHeapType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this just (*fa)->type == fb->type?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, that would be a good simplification.

Comment thread src/wasm/wasm-binary.cpp Outdated
return (*ma)->initial == mb->initial && (*ma)->max == mb->max &&
(*ma)->hasMax() == mb->hasMax() && (*ma)->shared == mb->shared &&
(*ma)->is64() == mb->is64() &&
(*ma)->pageSizeLog2 == mb->pageSizeLog2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have MemoryUtils::isSubType - we can check a <= b && b <= a here? (maybe adding a helper for it in MemoryUtils?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, or if that method isn't appropriate due to the runtime interaction Steven mentioned, I can create another helper.

Comment thread src/wasm/wasm-binary.cpp Outdated
auto* tb = std::get<Table*>(b);
return (*ta)->type == tb->type && (*ta)->initial == tb->initial &&
(*ta)->max == tb->max && (*ta)->hasMax() == tb->hasMax() &&
(*ta)->is64() == tb->is64();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have RuntimeTable::isSubType, which we could use like above? Though perhaps first with moving isSubType to TableUtils?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in these two comments I am trying to avoid duplicating this logic across the codebase, which would make later refactors harder/error prone)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW isSubType is in RuntimeTable because it's not possible to statically determine whether one table is a subtype of another. table.grow will affect the subtyping relationship and it would be wrong to try to compare two table definitions for subtyping (this is a bug we previously had in the interpreter).

This is checking if two table definitions are equivalent statically, so I think we could keep this logic separate or introduce a new helper function for it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. Perhaps we can share some of the logic between those, though?

Comment thread test/lit/d8/fuzz_shell_exceptions.wast Outdated
;; Build to a binary wasm.
;;
;; RUN: wasm-opt %s -o %t.wasm -q -all
;; RUN: wasm-opt %s -o %t.wasm -q -all --disable-compact-imports

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-all makes the output Wasm contain compact imports, but the V8 run later in this file does not enable compact imports. We could alternatively have fixed this by enabled compact imports in V8, which I suppose would have the benefit of showing that V8 parses them correctly. I'll change this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is --wasm-staging not enough, btw, for v8 to support compact imports? That would be more general if so.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out --wasm-staging is not enough. I'll specifically enable --wasm-compact-imports.

wasm_bytes = self.get_binary(wat, ['--enable-compact-imports'])
self.assertIn(b'\x03env\x00\x7e', wasm_bytes)
self.assertIn(b'\x04math\x00\x7f', wasm_bytes)
self.assertIn(b'\x06single\x02m1', wasm_bytes)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be good to add a code size test here, something like 1,000 procedurally-generated imports with the same module, and seeing how much smaller the binary size is with the feature enabled?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I guess that would catch accidental regressions.

Comment thread src/wasm/wasm-binary.cpp Outdated
Comment on lines +334 to +338
template<class... Ts> struct Overloaded : Ts... {
using Ts::operator()...;
};
template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already exists in utilities.h.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks! I thought I had seen it before.

Comment thread src/wasm/wasm-binary.cpp Outdated
Comment on lines +350 to +359
ModuleUtils::iterImportedFunctions(
*wasm, [&](Function* func) { imports.push_back(func); });
ModuleUtils::iterImportedGlobals(
*wasm, [&](Global* global) { imports.push_back(global); });
ModuleUtils::iterImportedTags(*wasm,
[&](Tag* tag) { imports.push_back(tag); });
ModuleUtils::iterImportedMemories(
*wasm, [&](Memory* memory) { imports.push_back(memory); });
ModuleUtils::iterImportedTables(
*wasm, [&](Table* table) { imports.push_back(table); });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to

ModuleUtils::iterImports(*wasm, [&](ImportItem import) { imports.push_back(import); }

Base automatically changed from compact-imports-text to main July 24, 2026 18:23

@stevenfontanella stevenfontanella left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if the fuzzer is happy

Comment thread src/wasm/wasm-binary.cpp Outdated
Comment on lines +358 to +382
if (a.index() != b.index()) {
return false;
}
o << U32LEB(kind) << U32LEB(getTypeIndex(func->type.getHeapType()));
});
ModuleUtils::iterImportedGlobals(*wasm, [&](Global* global) {
writeImportHeader(global);
o << U32LEB(int32_t(ExternalKind::Global));
writeType(global->type);
o << U32LEB(global->mutable_);
});
ModuleUtils::iterImportedTags(*wasm, [&](Tag* tag) {
writeImportHeader(tag);
o << U32LEB(int32_t(ExternalKind::Tag));
o << uint8_t(0); // Reserved 'attribute' field. Always 0.
o << U32LEB(getTypeIndex(tag->type));
});
ModuleUtils::iterImportedMemories(*wasm, [&](Memory* memory) {
writeImportHeader(memory);
o << U32LEB(int32_t(ExternalKind::Memory));
writeResizableLimits(memory->initial,
memory->max,
memory->hasMax(),
memory->shared,
memory->is64(),
memory->pageSizeLog2);
});
ModuleUtils::iterImportedTables(*wasm, [&](Table* table) {
writeImportHeader(table);
o << U32LEB(int32_t(ExternalKind::Table));
writeType(table->type);
writeResizableLimits(table->initial,
table->max,
table->hasMax(),
/*shared=*/false,
table->is64());
});
if (const auto* fa = std::get_if<Function*>(&a)) {
auto* fb = std::get<Function*>(b);
return (*fa)->type == fb->type;
}
if (const auto* ga = std::get_if<Global*>(&a)) {
auto* gb = std::get<Global*>(b);
return (*ga)->type == gb->type && (*ga)->mutable_ == gb->mutable_;
}
if (const auto* ta = std::get_if<Tag*>(&a)) {
auto* tb = std::get<Tag*>(b);
return (*ta)->type == tb->type;
}
if (const auto* ma = std::get_if<Memory*>(&a)) {
auto* mb = std::get<Memory*>(b);
return MemoryUtils::sameType(**ma, *mb);
}
if (const auto* ta = std::get_if<Table*>(&a)) {
auto* tb = std::get<Table*>(b);
return TableUtils::sameType(**ta, *tb);
}
return false;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this comes out cleaner as std::visit:

return std::visit(
      overloaded{
          [](Function* fa, Function* fb) { 
            return fa->type == fb->type; 
          },
          [](Global* ga, Global* gb) { 
            return ga->type == gb->type && ga->mutable_ == gb->mutable_; 
          },
          [](Tag* ta, Tag* tb) { 
            return ta->type == tb->type; 
          },
          [](Memory* ma, Memory* mb) { 
            return MemoryUtils::sameType(*ma, *mb); 
          },
          [](Table* ta, Table* tb) { 
            return TableUtils::sameType(*ta, *tb); 
          },
          [](const auto&, const auto&) { 
            return false; 
          }
      },
      a, b);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Comment thread test/unit/test_compact_imports.py Outdated


class CompactImportsTest(utils.BinaryenTestCase):
def get_binary(self, wat_str, flags=[]):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe avoid the mutable default param (flags)? Although there's no bug here since flags is never mutated, I think it's better to avoid it in case the code changes in the future or gets copy-pasted.

https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments

Comment thread src/wasm/wasm-binary.cpp
case ImportGroup::SharedAll: {
const auto& first = imports[group.start];
writeInlineString(getModule(first).view());
writeInlineString("");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Looks like it's the same as just o << 0?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but having an empty name here is how the encoding of compact imports is specified.

Comment thread src/wasm/wasm-binary.cpp Outdated
writeInlineString(getModule(item).view());
writeInlineString(getBase(item).view());
writeImportDesc(item);
continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the cases continue, so maybe we can change it to break to make the control flow more clear

Comment thread test/lit/d8/fuzz_shell_exceptions.wast Outdated
;; Run in node.
;;
;; RUN: v8 %S/../../../scripts/fuzz_shell.js -- %t.wasm | filecheck %s
;; RUN: v8 --wasm-compact-imports %S/../../../scripts/fuzz_shell.js -- %t.wasm | filecheck %s

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about removing this and adding explicit features to wasm-opt, above? That will make sense in the long term, while this will get obsolete and might need more changes for other future features.

Comment thread test/unit/test_compact_imports.py Outdated
with_compact = self.get_binary(wat, ['--enable-compact-imports'])
without_compact = self.get_binary(wat, ['--disable-compact-imports'])
self.assertEqual(len(with_compact), 2030)
self.assertEqual(len(without_compact), 8021)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe just check the ratio rather than an exact measurement? something like without/with >= 3.9

@kripken kripken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm otherwise

@tlively
tlively enabled auto-merge (squash) July 24, 2026 21:13
@tlively
tlively disabled auto-merge July 24, 2026 21:18
@tlively
tlively merged commit 51db223 into main Jul 24, 2026
16 checks passed
@tlively
tlively deleted the opportunistic-compact-imports branch July 24, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants